-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat: add customization bithuman gpu avatar endpoint handling #4390
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add customization bithuman gpu avatar endpoint handling #4390
Conversation
📝 WalkthroughWalkthroughRoutes avatar provisioning between the default BitHuman JSON API and custom endpoints (multipart/form-data). Adds strict base64 validation, async query parsing, unified retry sender, avatar image/ID handling, and conditional auth/header logic, with control flow selection in Changes
Sequence DiagramsequenceDiagram
participant Avatar as Avatar Service
participant Detector as Endpoint Detector
participant JSONPath as JSON Request Path
participant FormDataPath as FormData Request Path
participant HTTP as HTTP Client
Avatar->>Detector: _is_default_api_url()
alt default API
Detector-->>Avatar: true
Avatar->>JSONPath: _send_json_request(livekit_url, token, room)
JSONPath->>JSONPath: serialize image to base64\nbuild JSON payload\nset headers (Content-Type, api-secret)
JSONPath->>HTTP: _send_request_with_retry(headers, json_data)
else custom API
Detector-->>Avatar: false
Avatar->>FormDataPath: _parse_async_parameter_from_url()
Avatar->>FormDataPath: _send_formdata_request(livekit_url, token, room, async_mode)
FormDataPath->>FormDataPath: build multipart/form-data\nattach file/bytes or URL\nset Authorization: Bearer
FormDataPath->>HTTP: _send_request_with_retry(headers, form_data)
end
HTTP->>HTTP: perform retries/backoff
alt success
HTTP-->>Avatar: response
else failure
HTTP-->>Avatar: raise APIConnectionError
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 398-402: Remove trailing whitespace on the reported blank lines in
the avatar.py method that contains the "try:" block using
urlparse(self._api_url); specifically strip trailing spaces from the empty lines
around the try/except and subsequent blocks (the blank lines after the try line,
after parse_qs(parsed.query), and the other blank lines reported) so the file no
longer triggers Ruff W293; run a quick trim of trailing whitespace (or run
ruff/autopep8) and commit the cleaned file.
- Around line 378-386: The headers dict currently becomes dict[str, str | None]
because self._api_secret can be None; before constructing headers in the method
that calls self._send_request_with_retry, narrow self._api_secret to a str by
asserting or casting it to str (e.g., api_secret: str = cast(str,
self._api_secret) or check and raise if None), then use that local api_secret
when building headers = {"Content-Type": "application/json", "api-secret":
api_secret} so headers is typed as dict[str, str].
- Around line 326-337: The _is_default_api_url function currently uses substring
matching which can misclassify custom URLs; change it to parse self._api_url
(use urllib.parse.urlparse) and inspect the parsed.hostname, then return True
only if the hostname equals one of the default domains ("auth.api.bithuman.ai",
"api.bithuman.ai") or is a direct subdomain (e.g., hostname.endswith("." +
domain)) to avoid matching path/query fragments; keep handling when
self._api_url is None.
- Around line 313-321: The custom endpoint branch allows api_token auth via
_send_formdata_request/_is_default_api_url, but _start_cloud_agent (and __init__
cloud-mode validation) still unconditionally requires api_secret; move the
api_secret requirement out of the common path and enforce it only for the
JSON/default API branch so custom endpoints can use api_token-only setups.
Concretely: update _start_cloud_agent to check _is_default_api_url() and only
assert or raise when api_secret is missing for default/JSON flows, and relax the
cloud-mode validation in __init__ to allow custom endpoints without api_secret
(while keeping any existing checks for api_token presence); ensure references to
_parse_async_parameter_from_url remain unchanged.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings
Files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
livekit-plugins/livekit-plugins-anam/livekit/plugins/anam/avatar.py (1)
_ensure_http_session(61-65)
🪛 GitHub Actions: CI
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
[error] 384-384: mypy: Argument "headers" to "_send_request_with_retry" of "AvatarSession" has incompatible type "dict[str, str | None]"; expected "dict[str, str]"
🪛 GitHub Check: ruff
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
[failure] 437-437: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:437:1: W293 Blank line contains whitespace
[failure] 412-412: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:412:1: W293 Blank line contains whitespace
[failure] 402-402: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:402:1: W293 Blank line contains whitespace
[failure] 398-398: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:398:1: W293 Blank line contains whitespace
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Outdated
Show resolved
Hide resolved
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Show resolved
Hide resolved
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Show resolved
Hide resolved
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
148-154: Cloud mode validation still requiresapi_secretunconditionally.The
_send_formdata_requestmethod (line 505) allowsapi_tokenas an alternative toapi_secretfor custom endpoints, but__init__validation at line 151-152 blocks cloud mode withoutapi_secret. Users with custom endpoints who only haveapi_tokenwill fail initialization.🐛 Proposed fix
elif self._mode == "cloud": if not utils.is_given(avatar_image) and not utils.is_given(avatar_id): raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode") - if self._api_secret is None: - raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode") + # For custom endpoints, either api_token or api_secret is acceptable + # For default endpoints, api_secret is required (validated later in _send_json_request) + if self._api_secret is None and self._api_token is None: + raise BitHumanException( + "BITHUMAN_API_SECRET or BITHUMAN_API_TOKEN is required for cloud mode" + ) if self._api_url is None: - raise BitHumanException("BITHUMAN_API_URL are required for cloud mode") + raise BitHumanException("BITHUMAN_API_URL is required for cloud mode")Note: Also fixed the grammar ("are" → "is") in the error message.
🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 336-342: Replace the hostname extraction that uses parsed.netloc
with parsed.hostname so ports won’t break domain comparisons: in the method that
parses self._api_url (the block using parsed = urlparse(self._api_url) and
hostname = parsed.netloc or parsed.hostname), change it to use parsed.hostname
directly, keep the existing None check (if hostname is None: return False), and
then compare hostname against default_domains (["auth.api.bithuman.ai",
"api.bithuman.ai"]) as before.
🧹 Nitpick comments (5)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (5)
320-320: Line exceeds 100 characters.Per coding guidelines, the maximum line length is 100 characters. This line should be wrapped.
♻️ Proposed fix
- await self._send_formdata_request(livekit_url, livekit_token, room_name, async_mode=async_mode) + await self._send_formdata_request( + livekit_url, livekit_token, room_name, async_mode=async_mode + )
378-381: Unreachable code branch forbytestype.The
__init__method (lines 156-165) only sets_avatar_imagetoImage.Imageorstr, neverbytes. This branch is dead code.Consider removing this branch or updating
__init__to supportbytesinput if that's intended functionality.♻️ Proposed fix (remove dead code)
if isinstance(self._avatar_image, Image.Image): img_byte_arr = io.BytesIO() self._avatar_image.save(img_byte_arr, format="JPEG", quality=95) img_byte_arr.seek(0) import base64 json_data["image"] = base64.b64encode(img_byte_arr.getvalue()).decode("utf-8") - elif isinstance(self._avatar_image, bytes): - import base64 - - json_data["image"] = base64.b64encode(self._avatar_image).decode("utf-8") elif isinstance(self._avatar_image, str): json_data["image"] = self._avatar_image
468-476: Unreachable code branch forbytestype (same as_send_json_request).The
__init__method never sets_avatar_imagetobytes, making this branch dead code.♻️ Proposed fix (remove dead code)
if isinstance(self._avatar_image, Image.Image): # Convert PIL Image to bytes and upload as file img_byte_arr = io.BytesIO() self._avatar_image.save(img_byte_arr, format="JPEG", quality=95) img_byte_arr.seek(0) form_data.add_field( "avatar_image", img_byte_arr, filename="avatar.jpg", content_type="image/jpeg", ) - elif isinstance(self._avatar_image, bytes): - # Upload raw bytes as file - img_byte_arr = io.BytesIO(self._avatar_image) - form_data.add_field( - "avatar_image", - img_byte_arr, - filename="avatar.jpg", - content_type="image/jpeg", - ) elif isinstance(self._avatar_image, str):
494-496: Silent fallback to URL on base64 decode failure may mask errors.If the string is intended to be base64 but is malformed, silently treating it as a URL will cause a confusing server-side failure. Consider logging a warning or validating the string format before attempting decode.
♻️ Proposed fix
except Exception: - # If decode fails, treat as URL - form_data.add_field("avatar_image_url", self._avatar_image) + # If decode fails, log warning and treat as URL + logger.warning( + "Failed to decode avatar_image as base64, treating as URL", + extra={"avatar_image": self._avatar_image[:50] + "..." if len(self._avatar_image) > 50 else self._avatar_image}, + ) + form_data.add_field("avatar_image_url", self._avatar_image)
543-547: Passing bothjsonanddatatoaiohttp.post()is ambiguous.The docstring states
json_dataandform_dataare mutually exclusive, but both are always passed topost(). While aiohttp prioritizesjsonwhen both are provided, this implicit behavior is fragile. Consider adding a runtime check or only passing the non-None parameter.♻️ Proposed fix
+ if json_data is not None and form_data is not None: + raise ValueError("json_data and form_data are mutually exclusive") + for i in range(self._conn_options.max_retry): try: - async with self._ensure_http_session().post( - self._api_url, - headers=headers, - json=json_data, - data=form_data, - timeout=aiohttp.ClientTimeout(sock_connect=self._conn_options.timeout), - ) as response: + kwargs: dict[str, Any] = { + "headers": headers, + "timeout": aiohttp.ClientTimeout(sock_connect=self._conn_options.timeout), + } + if json_data is not None: + kwargs["json"] = json_data + elif form_data is not None: + kwargs["data"] = form_data + + async with self._ensure_http_session().post( + self._api_url, + **kwargs, + ) as response:Note: You'll need to add
from typing import Anyif not already imported.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings
Files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues
Applied to files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: type-check (3.13)
- GitHub Check: type-check (3.9)
- GitHub Check: unit-tests
🔇 Additional comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
402-426: LGTM!The method correctly parses the async query parameter with appropriate fallback behavior. The return type annotation and docstring follow the project conventions.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 476-495: The code currently treats any non-URL string as base64
and decodes it, which can produce corrupted uploads; update the logic in the
branch handling self._avatar_image (the string path that sets "avatar_image_url"
vs "avatar_image" in form_data) to first validate that the string is valid
base64 before decoding (e.g., check with a base64 regex or use
base64.b64decode(..., validate=True) and ensure proper padding/length), only
then decode to bytes and create the io.BytesIO to add_field("avatar_image"); if
validation fails, fall back to adding as "avatar_image_url" and keep the
existing exception handling for safety.
🧹 Nitpick comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
521-565: Add a guard to enforce payload mutual exclusivity.
Helps catch accidental misuse early.♻️ Proposed refactor
async def _send_request_with_retry( self, headers: dict[str, str], json_data: dict | None = None, form_data: aiohttp.FormData | None = None, ) -> None: @@ - for i in range(self._conn_options.max_retry): + if (json_data is None) == (form_data is None): + raise ValueError("Exactly one of json_data or form_data must be provided") + + for i in range(self._conn_options.max_retry):
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings
Files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues
Applied to files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (6)
livekit-agents/livekit/agents/utils/misc.py (1)
is_given(25-26)livekit-plugins/livekit-plugins-anam/livekit/plugins/anam/avatar.py (1)
_ensure_http_session(61-65)livekit-plugins/livekit-plugins-bey/livekit/plugins/bey/avatar.py (1)
_ensure_http_session(63-67)livekit-plugins/livekit-plugins-hedra/livekit/plugins/hedra/avatar.py (1)
_ensure_http_session(69-73)livekit-plugins/livekit-plugins-simli/livekit/plugins/simli/avatar.py (1)
_ensure_http_session(79-83)livekit-plugins/livekit-plugins-tavus/livekit/plugins/tavus/avatar.py (1)
_ensure_http_session(57-61)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: unit-tests
- GitHub Check: type-check (3.9)
- GitHub Check: type-check (3.13)
🔇 Additional comments (5)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (5)
9-9: Import update looks good.
Required for the new URL parsing helpers.
312-325: Clear branching between default and custom endpoints.
Flow is easy to follow and keeps responsibilities separated.
348-399: JSON request path looks solid.
Base64 handling and headers are consistent with the default API flow.
401-425: Async query parsing is clear and safe.
Graceful fallback on parse errors is appropriate here.
327-347: Normalize hostname to avoid case-sensitive misclassification.
If the URL includes uppercase letters, the current comparison can route to the wrong flow.🔧 Proposed fix
try: parsed = urlparse(self._api_url) - hostname = parsed.hostname + hostname = parsed.hostname if hostname is None: return False - default_domains = ["auth.api.bithuman.ai", "api.bithuman.ai"] - return hostname in default_domains + default_domains = {"auth.api.bithuman.ai", "api.bithuman.ai"} + return hostname.lower() in default_domainsLikely an incorrect or invalid review comment.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 516-538: The __init__ currently rejects avatar_image values that
are neither file paths nor URLs, preventing valid base64 strings from reaching
the branch that handles them; update the avatar_image validation inside __init__
to also accept strings where _is_valid_base64(self._avatar_image) is True (in
addition to existing path/URL checks) so base64 inputs are stored and later
processed by the block referencing _avatar_image and _is_valid_base64; ensure
the check uses the same _is_valid_base64 helper and does not treat valid base64
as an invalid type that raises an exception.
- Around line 65-82: The function _is_valid_base64 contains blank lines with
trailing whitespace causing Ruff W293; open the _is_valid_base64 implementation
and remove trailing spaces from the blank lines (or run ruff/autoformat) so no
lines in that function end with extraneous whitespace, leaving the logic
(s_clean trimming, empty check, length % 4 check, and base64_pattern match)
unchanged.
♻️ Duplicate comments (2)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (2)
352-365: Custom endpoints are still blocked by cloud-modeapi_secretvalidation.This branch supports
api_tokenauth for custom endpoints, but__init__still raises whenapi_secretis missing, which prevents token-only configurations from reaching this path. Consider gating theapi_secretrequirement to default endpoints (or requiring either token or secret for custom endpoints).🔧 Suggested fix (in __init__ cloud-mode validation)
elif self._mode == "cloud": if not utils.is_given(avatar_image) and not utils.is_given(avatar_id): raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode") - if self._api_secret is None: - raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode") + if self._is_default_api_url(): + if self._api_secret is None: + raise BitHumanException("BITHUMAN_API_SECRET is required for default API") + elif self._api_secret is None and self._api_token is None: + raise BitHumanException( + "BITHUMAN_API_TOKEN or BITHUMAN_API_SECRET is required for custom endpoints" + ) if self._api_url is None: raise BitHumanException("BITHUMAN_API_URL are required for cloud mode")
428-433: Narrowapi_secrettostrbefore building headers.The assert only excludes
None; strict typing can still see a non-strsentinel and producedict[str, str | NotGiven]. This can break mypy strict and send invalid headers at runtime.✅ Proposed fix
- assert self._api_secret is not None, "api_secret is required for default API" - - headers = { - "Content-Type": "application/json", - "api-secret": self._api_secret, - } + api_secret = self._api_secret + if not isinstance(api_secret, str): + raise BitHumanException("BITHUMAN_API_SECRET is required for default API") + headers = { + "Content-Type": "application/json", + "api-secret": api_secret, + }To verify the sentinel’s behavior, inspect
NotGiven/NOT_GIVENdefinitions and truthiness:#!/bin/bash rg -n -C3 "class NotGiven|NOT_GIVEN" rg -n -C3 "def is_given"
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings
Files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues
Applied to files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
livekit-agents/livekit/agents/utils/misc.py (1)
is_given(25-26)
🪛 GitHub Check: ruff
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
[failure] 82-82: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:82:1: W293 Blank line contains whitespace
[failure] 77-77: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:77:1: W293 Blank line contains whitespace
[failure] 72-72: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:72:1: W293 Blank line contains whitespace
[failure] 68-68: Ruff (W293)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:68:1: W293 Blank line contains whitespace
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: type-check (3.9)
- GitHub Check: unit-tests
🔇 Additional comments (3)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (3)
367-387: Default endpoint detection via hostname looks good.The hostname-based check is clear and reduces false positives.
441-465: Async flag parsing looks good.Graceful handling of missing/invalid query parameters is appropriate here.
564-608: Unified retry sender looks good.Cleanly consolidates JSON and form-data flows with consistent retry behavior.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Outdated
Show resolved
Hide resolved
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (2)
199-205: Base64 string inputs are rejected during initialization, but supported later.The
_send_formdata_requestmethod (lines 522-537) handles base64 strings via_is_valid_base64, but this code path rejects any string that isn't a file path or URL. Base64 avatar images will fail at init before reaching the formdata handler.🐛 Suggested fix
elif isinstance(avatar_image, str): if os.path.exists(avatar_image): self._avatar_image = Image.open(avatar_image) elif avatar_image.startswith("http"): self._avatar_image = avatar_image + elif _is_valid_base64(avatar_image): + self._avatar_image = avatar_image else: raise BitHumanException(f"Invalid avatar image: {avatar_image}")
188-194: Cloud mode validation blocksapi_token-only setups for custom endpoints.The validation at line 191-192 unconditionally requires
api_secretfor cloud mode. However,_send_formdata_request(lines 549-554) supportsapi_tokenas an alternative for custom endpoints. This blocks legitimate use cases where custom endpoints use onlyapi_token.Also, minor grammar: "are required" → "is required".
🐛 Suggested fix (relax validation for custom endpoints)
elif self._mode == "cloud": if not utils.is_given(avatar_image) and not utils.is_given(avatar_id): raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode") - if self._api_secret is None: - raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode") + # Custom endpoints can use api_token, default API requires api_secret + is_default_url = self._api_url is None or any( + domain in self._api_url for domain in ["auth.api.bithuman.ai", "api.bithuman.ai"] + ) + if is_default_url and self._api_secret is None: + raise BitHumanException("BITHUMAN_API_SECRET is required for default BitHuman API") + elif not is_default_url and self._api_secret is None and self._api_token is None: + raise BitHumanException( + "BITHUMAN_API_SECRET or BITHUMAN_API_TOKEN is required for custom endpoints" + ) if self._api_url is None: - raise BitHumanException("BITHUMAN_API_URL are required for cloud mode") + raise BitHumanException("BITHUMAN_API_URL is required for cloud mode")
🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 420-423: The bytes branch under the serialization logic is dead
because self._avatar_image is typed as Image.Image | str | None and no code sets
bytes; remove the unreachable elif block (the "elif
isinstance(self._avatar_image, bytes):" branch including its base64 import and
assignment to json_data["image"]) from the method that builds json_data so only
the Image.Image and str branches remain; if bytes input should be supported
instead, update the _avatar_image type and any setters to accept bytes and add a
test, otherwise delete this branch and any now-unused imports.
- Around line 538-540: The fallback silently treating non-URL, non-base64
strings as a URL is unsafe; in the method handling avatar upload (look for the
code using self._avatar_image and the form_data.add_field("avatar_image_url",
...)), replace that else branch with explicit validation: if self._avatar_image
does not start with "http://" or "https://" and is not valid base64, raise a
ValueError (or a specific InvalidAvatarError) instead of adding it to form_data
as "avatar_image_url"; keep the existing branches that add "avatar_image" for
base64 data and "avatar_image_url" for valid URLs unchanged.
♻️ Duplicate comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
509-517: Same unreachable code -bytestype check.This branch has the same issue as
_send_json_request:_avatar_imagecannot be bytes per its type annotation.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings
Files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues
Applied to files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧬 Code graph analysis (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (6)
livekit-agents/livekit/agents/utils/misc.py (1)
is_given(25-26)livekit-plugins/livekit-plugins-anam/livekit/plugins/anam/avatar.py (1)
_ensure_http_session(61-65)livekit-plugins/livekit-plugins-bey/livekit/plugins/bey/avatar.py (1)
_ensure_http_session(63-67)livekit-plugins/livekit-plugins-hedra/livekit/plugins/hedra/avatar.py (1)
_ensure_http_session(69-73)livekit-plugins/livekit-plugins-simli/livekit/plugins/simli/avatar.py (1)
_ensure_http_session(79-83)livekit-plugins/livekit-plugins-tavus/livekit/plugins/tavus/avatar.py (1)
_ensure_http_session(57-61)
🔇 Additional comments (5)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (5)
52-90: LGTM - Base64 validation logic is sound.The validation covers the key checks: non-empty, proper length, character pattern, and successful decoding. The combination of regex pre-check and decode attempt provides good protection against misclassifying non-base64 strings.
352-367: LGTM - Endpoint routing logic is clean.The routing correctly branches between custom (FormData) and default (JSON) endpoints, with
api_secretrequirement appropriately scoped to the default API path.
369-388: LGTM - Hostname-based URL detection is correct.Using
parsed.hostnameavoids the port-matching issue withnetloc, and the fallback to substring matching provides resilience.
443-467: LGTM - Async parameter parsing is straightforward.The method correctly extracts the
asyncquery parameter with appropriate fallback behavior.
566-610: LGTM - Unified retry mechanism is well-structured.The method cleanly abstracts retry logic for both JSON and FormData payloads with appropriate error handling and logging.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Outdated
Show resolved
Hide resolved
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py`:
- Around line 520-524: Catch the original exception into a variable and use
exception chaining when re-raising BitHumanException so the original traceback
is preserved; change the handler to `except Exception as err:` and raise
`BitHumanException(...) from err` (the code references `self._avatar_image` and
raises `BitHumanException`) so the original error context is attached to the new
exception.
🧹 Nitpick comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
63-63: Consider movingreimport to module level.The
remodule is imported inside the function on each call. For consistency with other imports (likebase64at line 4), consider moving it to the module-level imports.♻️ Suggested change
At the top of the file (around line 10):
import reThen remove line 63 from the function.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings
Files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues
Applied to files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🪛 GitHub Check: ruff
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
[failure] 522-524: Ruff (B904)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py:522:21: B904 Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: unit-tests
- GitHub Check: type-check (3.13)
🔇 Additional comments (7)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (7)
4-10: LGTM!The imports are appropriate for the new base64 validation and URL parsing functionality.
188-194: Cloud mode validation may block api_token-only setups for custom endpoints.The
_send_formdata_requestmethod allows eitherapi_tokenorapi_secret(line 539:auth_token = self._api_token or self._api_secret), but the__init__validation unconditionally requiresapi_secretfor cloud mode. Users wanting to use custom endpoints with onlyapi_tokenwill be blocked at initialization.Also, minor grammar fix: "are required" → "is required".
🐛 Suggested fix
elif self._mode == "cloud": if not utils.is_given(avatar_image) and not utils.is_given(avatar_id): raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode") - if self._api_secret is None: - raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode") + if self._api_secret is None and self._api_token is None: + raise BitHumanException( + "BITHUMAN_API_SECRET or BITHUMAN_API_TOKEN is required for cloud mode" + ) if self._api_url is None: - raise BitHumanException("BITHUMAN_API_URL are required for cloud mode") + raise BitHumanException("BITHUMAN_API_URL is required for cloud mode")Please verify this is the intended behavior for custom endpoint authentication.
199-207: LGTM!The avatar image handling now correctly accepts file paths, HTTP(S) URLs, and valid base64 strings with proper validation.
354-391: LGTM!Good separation of concerns between custom and default endpoints. The
api_secretassertion is now correctly scoped to the default API branch, and_is_default_api_urlproperly uses hostname parsing.
392-437: LGTM!The JSON request handling is well-structured with proper avatar image serialization and the
api_secretassertion ensures type safety for the headers dictionary.
439-463: LGTM!Clean implementation for parsing the async parameter from URL query strings with proper error handling.
556-600: LGTM!Well-designed unified retry mechanism that handles both JSON and FormData payloads with proper error logging and retry intervals.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
188-194: Cloud-mode validation blocksapi_token-only setups for custom endpoints.Lines 191-192 unconditionally require
api_secretfor all cloud mode, but_send_formdata_request(lines 539-544) accepts eitherapi_tokenorapi_secret. Users with custom endpoints who only haveapi_tokenwill fail at initialization before reaching the custom endpoint code path.Additionally, there's a grammar issue: "are required" should be "is required".
🐛 Proposed fix
elif self._mode == "cloud": if not utils.is_given(avatar_image) and not utils.is_given(avatar_id): raise BitHumanException("`avatar_image` or `avatar_id` must be set for cloud mode") - if self._api_secret is None: - raise BitHumanException("BITHUMAN_API_SECRET are required for cloud mode") + # Custom endpoints can use api_token, default API requires api_secret + # Full validation deferred to request methods + if self._api_secret is None and self._api_token is None: + raise BitHumanException( + "BITHUMAN_API_SECRET or BITHUMAN_API_TOKEN is required for cloud mode" + ) if self._api_url is None: - raise BitHumanException("BITHUMAN_API_URL are required for cloud mode") + raise BitHumanException("BITHUMAN_API_URL is required for cloud mode")
🧹 Nitpick comments (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (1)
556-561: Consider more precise type annotation forjson_data.The
dicttype is generic. For stricter mypy compliance, consider usingdict[str, Any].♻️ Suggested improvement
async def _send_request_with_retry( self, headers: dict[str, str], - json_data: dict | None = None, + json_data: dict[str, Any] | None = None, form_data: aiohttp.FormData | None = None, ) -> None:You'll need to add
Anyto the imports:-from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings
Files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
🧠 Learnings (1)
📚 Learning: 2026-01-16T07:44:56.353Z
Learnt from: CR
Repo: livekit/agents PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-01-16T07:44:56.353Z
Learning: Applies to **/*.py : Run ruff linter and auto-fix issues
Applied to files:
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: type-check (3.13)
- GitHub Check: type-check (3.9)
- GitHub Check: unit-tests
🔇 Additional comments (7)
livekit-plugins/livekit-plugins-bithuman/livekit/plugins/bithuman/avatar.py (7)
53-90: LGTM!The strict base64 validation logic is well-implemented with proper length checks, regex validation, and actual decode verification. Good use of Google-style docstring.
199-207: LGTM!Good validation flow: file path → URL → base64 → error. This properly accepts all valid avatar image formats while rejecting invalid inputs.
349-369: LGTM!Good refactoring to separate custom endpoint (form-data) and default API (JSON) paths. The
api_secretrequirement is correctly enforced only for the default API branch.
371-391: LGTM!Good use of
parsed.hostnameinstead ofnetlocto handle URLs with ports. The exception fallback provides reasonable safety.
392-437: LGTM!Well-structured JSON request method. The assertion at line 426 ensures type narrowing for
api_secret, addressing the previous mypy issue.
439-463: LGTM!Clean implementation for optional async parameter parsing. Silent exception handling is appropriate here since this is an optional enhancement.
465-554: LGTM!Well-implemented form-data request method with proper exception chaining (
from err), strict validation for invalid avatar strings, and flexible auth token handling.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.